using System;
using System.Collections.Generic;
using ADDDF.Infrastructure.Configuration;
using System.Configuration;
using System.Reflection;

namespace ADDDF.Infrastructure
{
    public static class RepositoryFactory
    {
        /// <summary>
        /// Store interface short name as key, and type fullname as value.
        /// </summary>
        private static Dictionary<string, string> _interfaceToType = new Dictionary<string, string>();

        /// <summary>
        /// Gets or creates an instance of the requested interface.  Once a 
        /// repository is created and initialized, it is cached, and all 
        /// future requests for the repository will come from the cache.
        /// </summary>
        /// <typeparam name="TRepository">The interface of the repository 
        /// to create.</typeparam>
        /// <typeparam name="TEntity">The type of the Entity that the 
        /// repository is for.</typeparam>
        /// <returns>An instance of the interface requested.</returns>
        public static TRepository Create<TRepository, TEntity>() 
            where TRepository : IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            // Initialize the provider's default value
            TRepository repository = default(TRepository);

            // Get the type to be created
            Type repositoryType = FindTypeInInterfaceDictionary(typeof(TRepository));

            if (repositoryType == null)
            {
                // Get the repositoryMappingsConfiguration config section
                RepositorySettings settings = (RepositorySettings)ConfigurationManager.GetSection(RepositoryMappingConstants.RepositoryMappingsConfigurationSectionName);

                repositoryType = FindTypeInRepositoryMappings(settings, typeof(TRepository));

                if (repositoryType == null)
                    repositoryType = FindTypeInRepositoryAssemblies(settings, typeof(TRepository));
            
                if (repositoryType == null)
                    repositoryType = FindTypeInRepositoryMappingsUsingEntityName(settings, typeof(TEntity));
            }

            if (repositoryType == null)
            {
                throw new ApplicationException("Unable to determine the concrete repository type for '"
                    + typeof(TRepository).Name + "'");
            }
            
            // See if an IUnitOfWork needs to be injected to the repository's constructor
            object[] constructorArgs = null;

            try
            {
                // Create the repository, and cast it to the interface specified
                repository = (TRepository)Activator.CreateInstance(repositoryType, constructorArgs);
            }
            catch (Exception ex)
            {
                throw new ApplicationException("Unable to create the repository instance '"
                    + repositoryType.FullName + "' for '" + typeof(TRepository).FullName + "'. "
                    + "The constructor might not support the required argument list.", ex);
            }

            return repository;
        }

        /// <summary>
        /// Register a repository interface with a concrete type. This takes precedence over config settings.
        /// </summary>
        /// <typeparam name="TRepository"></typeparam>
        /// <typeparam name="TEntity"></typeparam>
        /// <param name="repositoryType"></param>
        public static void Register<TRepository, TEntity>(Type repositoryType)
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            if (_interfaceToType.ContainsKey(typeof(TRepository).Name))
            {
                _interfaceToType[typeof(TRepository).Name] = repositoryType.AssemblyQualifiedName;
            }
            else
            {
                _interfaceToType.Add(typeof(TRepository).Name, repositoryType.AssemblyQualifiedName);
            }
        }

        /// <summary>
        /// Unregister an interface.
        /// </summary>
        /// <typeparam name="TRepository"></typeparam>
        /// <typeparam name="TEntity"></typeparam>
        public static void Unregister<TRepository, TEntity>(Type repositoryType)
            where TRepository : class, IRepository<TEntity>
            where TEntity : IAggregateRoot
        {
            if (_interfaceToType.ContainsKey(typeof(TRepository).Name))
            {
                if (_interfaceToType[typeof(TRepository).Name] == repositoryType.AssemblyQualifiedName)
                    _interfaceToType.Remove(typeof(TRepository).Name);
                else
                    throw new ArgumentException("Cannot find given repository type");
            }
        }

        #region Type Locator Functions

        private static Type FindTypeInInterfaceDictionary(Type repositoryInterfaceType)
        {
            string interfaceShortName = repositoryInterfaceType.Name;

            if (_interfaceToType.ContainsKey(interfaceShortName))
            {
                return Type.GetType(_interfaceToType[interfaceShortName]);
            }

            return null;
        }

        private static Type FindTypeInRepositoryMappings(RepositorySettings settings, Type repositoryInterfaceType)
        {
            string interfaceShortName = repositoryInterfaceType.Name;

            // Get the type to be created
            Type repositoryType = null;

            // See if a valid interfaceShortName was passed in
            if (settings.RepositoryMappings.ContainsKey(interfaceShortName))
            {
                repositoryType = Type.GetType(settings.RepositoryMappings[interfaceShortName].RepositoryFullTypeName);

                if (repositoryType == null)
                {
                    throw new ApplicationException("Type.GetType failed to get type: '" +
                        settings.RepositoryMappings[interfaceShortName].RepositoryFullTypeName + "'. " +
                        "Check RepositoryMappings in the config file.");
                }
            }

            return repositoryType;
        }

        private static Type FindTypeInRepositoryMappingsUsingEntityName(RepositorySettings settings, Type entityType)
        {
            string entityTypeName = entityType.Name;

            // Get the type to be created
            Type repositoryType = null;

            // Check to see if any of the interface short names contain the name of the Entity.  
            // If they do, then use the matching one to get the right Repository Mapping Element.
            foreach (RepositoryMappingElement element in settings.RepositoryMappings)
            {
                if (element.InterfaceShortTypeName.Contains(entityTypeName))
                {
                    repositoryType = Type.GetType(settings.RepositoryMappings[element.InterfaceShortTypeName].RepositoryFullTypeName);
                    break;
                }
            }

            return repositoryType;
        }

        private static Type FindTypeInRepositoryAssemblies(RepositorySettings settings, Type repositoryType)
        {
            var assElements = settings.RepositoryAssemblies;

            foreach (RepositoryAssemblyElement assElement in assElements)
            {
                Assembly ass = Assembly.Load(assElement.Name);

                Type[] typesInAss = ass.GetExportedTypes();

                foreach (Type type in typesInAss)
                {
                    if (!type.IsClass || type.IsAbstract)
                        continue;

                    if (repositoryType.IsAssignableFrom(type))
                        return type;
                }
            }

            return null;
        }

        #endregion
    }
}
